#!/usr/bin/env python
# -*- coding: utf-8 -*-

__author__ = 'Dan McDougall <YouKnowWho@YouKnowWhat.com>'

# Note:  This example requires Python 2.6 but it can be trivially
#        modified to work with Python 2.3+
import os, json
from string import Template
import cherrypy

"""cherrypy_jqgrid.py: This is an example of how to use the jqGrid jQuery
plugin with CherryPy.  It includes helper functions to turn lists into
jqGrid-friendly JSON objects and methods to search/sort said lists.
This example is self-running and will start up a CherryPy server that
displays a jqGrid page.

jQuery:  http://jquery.com/
jqGrid:  http://www.trirand.com/blog/"""

# Important note regarding the "list of lists" mentioned below...
#   When I say, "list of lists" I mean something like this:
#
#   [
#       ['1', 'John', 'Doe'],
#       ['2', 'Jane', 'Smith']
#   ]
#
#   ...where the list contains a list of cells in your grid.

# For this example I'm using the google hosted versions of jquery and jquery-ui
# To cut down on line length I've split up the strings =)
jquery_url = '<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"'
jquery_url += ' type="text/javascript"></script>'
jquery_ui_url ='<script src='
jquery_ui_url += '"http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js" '
jquery_ui_url += 'type="text/javascript"></script>'
jquery_ui_css = '<link rel="stylesheet" type="text/css" media="screen" href="'
jquery_ui_css += 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/black-tie/jquery-ui.css'
jquery_ui_css += '" />'

# These are all helper functions to make the serving up jqGrids via CherryPy much simpler
def split_seq(seq, size):
    """Split up sequence, 'seq' into pieces of 'size' (for jqGrid pagination)

    From the perspective of jqGrid, 'size' maps to the 'rows' parameter which
    sets the number of rows to display in the grid."""
    return [seq[i:i+size] for i in range(0, len(seq), size)]

def sortby(cells, field_index):
    """Sorts 'cells' (which should be a list of lists) by field_index in
    ascending (asc) order.

    If you need descending order (desc) just do a _list.reverse() after."""
    cells.sort(lambda x,y:cmp(x[field_index], y[field_index]))
    return cells

def sortby_field(sort_field, header, cells):
    """Sorts a list of lists (your jqGrid cells) by 'sort_field' given a
    'header' that corresponds to each field in 'cells' and returns the result.

    @sort_field: The column you're sorting by.  Must match up with 'header'
    @header: Something like ['primary_key', 'firstname', 'lastname'] that
             matches your list of cells (cells).
    @cells: The list of lists representing your jqGrid cells."""
    sorted_list = []
    for column in header:
        if sort_field == column:
            sorted_list = sortby(cells, header.index(column))
    return sorted_list

def jqgrid_format(header, cells, id_field_index=None):
    """Convert a list of lists into a format that jqGrid will understand
    when it is rendered into JSON.

    If an 'id_field_index' is given, that field will be used as jqGrid's
    'id' parameter."""
    out_list = []
    for row in cells:
        if id_field_index:
            key = row[id_field_index] # Use this field as the 'id' field
        else:
            key = row[0] # Otherwise, just use the first field
        out_cells = []
        for item in row:
            out_cells.append(item)
        out_list.append({'id': key, 'cell': out_cells})
    return out_list

def search_cells(header, cells, search_string, search_field, search_type):
    """Given a list of lists (cells) and a header (matching the cells),
    returns a list of items that match the given search_string, search_field,
    and search_type.

    Valid search types:
    eq - Equal to
    ne - Not equal to
    bw - Begins with
    ew - Ends with
    lt - Less than
    le - Less than or equal to
    gt - Greater than
    ge - Greater than or equal to
    cn - Contains"""
    out_list = []
    for item in cells:
        if search_type == "bw":
            if str(item[header.index(search_field)]).startswith(search_string):
                out_list.append(item)
        elif search_type == "ew":
            if str(item[header.index(search_field)]).endswith(search_string):
                out_list.append(item)
        elif search_type == "eq":
            if str(item[header.index(search_field)]) == search_string:
                out_list.append(item)
        elif search_type == "ne":
            if str(item[header.index(search_field)]) != search_string:
                out_list.append(item)
        elif search_type == "lt":
            if item[header.index(search_field)] < int(search_string):
                out_list.append(item)
        elif search_type == "le":
            if item[header.index(search_field)] <= int(search_string):
                out_list.append(item)
        elif search_type == "gt":
            if item[header.index(search_field)] > int(search_string):
                out_list.append(item)
        elif search_type == "ge":
            if item[header.index(search_field)] >= int(search_string):
                out_list.append(item)
        elif search_type == "cn":
            if search_string in item[header.index(search_field)]:
                out_list.append(item)
    return out_list

def jqgrid_json(self, cell_list, header, id_field_index=None, rows=None, sidx=None, _search=False,
    searchField=None, searchOper=None, searchString=None, page=None, sord=None):
    """Returns a list of cells encoded in a format jqgrid understands.

    Required variables:
    @header: The column names (must match cell_list).  Example:
        header = ['primary_key', 'firstname', 'lastname']
    @cell_list: A list containing lists of cells.  Example:
        cell_list = [
            ['1', 'John', 'Smith'],
            ['2', 'Bob', 'Jones']
        ]

    Optional variables:
    @rows: Number of rows to return.  If this is not set all rows will be returned.
    @sidx: Field to sort the output by.
    @sord: Sort order.  Either 'asc' (ascending) or 'desc' (descending).
    @_search: True or False (whether or not this is a search).  Default is False.
              Also takes strings of 'true' and 'false'.
    @searchField: The field we're searching on.
    @searchOper: The type of search (eq = equals, cn = contains, etc).
    @searchString: The string to match for our search.
    @page: The page number to return (if paginating)."""
    if not page:
        page = 1
    if sidx: # Sort by this field
        cell_list = sortby_field(sidx, header, cell_list)
        if sord == 'desc':
            cell_list.reverse()
    if _search == "true" or _search is True: # We're (also) doing a search
        cell_list = search_cells(header, cell_list, searchString, searchField, searchOper)
    line_count = len(cell_list)
    jqgrid_cells = jqgrid_format(header, cell_list, id_field_index)
    if rows: # Limit the results to the number represented by the 'rows' parameter
        # Divide up the results into chunks of 'rows' size...
        paginated_list = split_seq(jqgrid_cells,int(rows))
        total_pages = len(paginated_list)
        if total_pages == 0: # No records to return
            jqgrid_dict = { # Construct an empty (but valid) JSON dict
                'total': total_pages, # This will be 0
                'records': line_count, # Ditto
                'page': page, # Always going to be 1
                'rows': ''}
        else:
            # Make the first item empty so page 1 equals list[1] (for convenience)
            paginated_list.insert(0,'')
            jqgrid_dict = { # Construct a jqgrid json dict
                'total': total_pages,
                'records': line_count,
                'page': page,
                'rows': paginated_list[int(page)]}
    else: # Return all records (don't paginate)
        jqgrid_dict = { # Same as above but without pagination
        'total': line_count,
        'records': line_count,
        'page': page, # Everything is on page 1
        'rows': jqgrid_list}
    return json.dumps(jqgrid_dict)

# Our jqGrid javascript
jqgrid_js = """
<script type="text/javascript">
$(document).ready(function(){
// This is a basic jqGrid with a navGrid (that adds a search button)
    jQuery('#users').jqGrid({
        url:'/users_json',
        datatype: "json",
        colNames:['User', 'Password', 'UID', 'GID', 'Comment', 'Home Directory', 'Shell'],
        colModel:[
            {name:'user',index:'user'},
            {name:'password',index:'password', width:110},
            {name:'uid',index:'uid', width:80},
            {name:'gid',index:'gid', width:80},
            {name:'gecos',index:'gecos'},
            {name:'homedir',index:'homedir'},
            {name:'shell',index:'shell'}
        ],
        rowNum:10,
        rowList:[10,20,30],
        multiselect: false,
        width: 700,
        height: "100%",
        pager: jQuery('#pusers'),
        sortname: 'uid',
        viewrecords: true,
        sortorder: "asc",
        caption: "Users"
    }).navGrid("#pusers",{edit:false,add:false,del:false},{},{width:350},{});
});
</script>
"""
# Test data (taken from an Ubuntu /etc/passwd file)
test_data = """
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
dhcp:x:100:101::/nonexistent:/bin/false
syslog:x:101:102::/home/syslog:/bin/false
klog:x:102:103::/home/klog:/bin/false
messagebus:x:103:109::/var/run/dbus:/bin/false
hplip:x:104:7:HPLIP system user,,,:/var/run/hplip:/bin/false
avahi-autoipd:x:105:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
avahi:x:106:114:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
haldaemon:x:107:116:Hardware abstraction layer,,,:/home/haldaemon:/bin/false
gdm:x:108:118:Gnome Display Manager:/var/lib/gdm:/bin/false
riskable:x:1000:1000:Riskable,,,:/home/riskable:/bin/bash
libuuid:x:110:120::/var/lib/libuuid:/bin/sh
zeroinst:x:111:121::/home/zeroinst:/bin/false
pulse:x:112:122:PulseAudio daemon,,,:/var/run/pulse:/bin/false
polkituser:x:113:125:PolicyKit,,,:/var/run/PolicyKit:/bin/false
clamav:x:114:129::/var/lib/clamav:/bin/false
mysql:x:115:131:MySQL Server,,,:/var/lib/mysql:/bin/false
sshd:x:116:65534::/var/run/sshd:/usr/sbin/nologin
nx:x:109:132::/var/lib/nxserver/home:/usr/lib/nx/nxserver
debian-tor:x:117:133::/var/lib/tor:/bin/bash
landscape:x:118:65534:Landscape Client Daemon,,,:/var/lib/landscape:/bin/false
uml-net:x:119:134::/home/uml-net:/bin/false
dnsmasq:x:120:65534:dnsmasq,,,:/var/lib/misc:/bin/false
vde2-net:x:121:136::/var/run/vde2:/bin/false
privoxy:x:122:65534::/etc/privoxy:/bin/false
apt-p2p:x:123:65534::/var/cache/apt-p2p:/bin/false
debian-transmission:x:124:137::/home/debian-transmission:/bin/false
saned:x:125:138::/home/saned:/bin/false
munin:x:126:140::/var/lib/munin:/bin/false
snmp:x:127:65534::/var/lib/snmp:/bin/false
"""

def test_data_to_list(test_data=test_data):
    """Turn the test data into a list of lists (the format we want)"""
    out_list = []
    for line in test_data.splitlines():
        if line: # skip blank lines
            line = line.split(':') # Turn the line into a list
            # Now turn any stringified integers back into integers for proper sorting
            for index, item in enumerate(line):
                try:
                    line[index] = int(item)
                except:
                    pass
            out_list.append(line)
    return out_list

# Now for the generic CherryPy app
class jqGrid(object):
    """Our CherryPy jqGrid application class"""
    @cherrypy.expose
    def index(self):
        """Serve up the page that will render the grid"""
        html = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Riskable's CherryPy jqGrid Example</title>
${jquery_ui_css}
<link rel="stylesheet" type="text/css" media="screen" href="ui.jqgrid.css" />
${jquery_url}
${jquery_ui_url}
<script src="grid.locale-en.js" type="text/javascript"></script>
<script src="jquery.jqGrid.min.js" type="text/javascript"></script>
${jqgrid_js}
</head>
<h2>jqGrid served up via CherryPy</h2>
<table id="users" class="scroll" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>
<div id="pusers" class="scroll" style="text-align:center;"></div>
</html>
        """
        t = Template(html)
        page = t.substitute(
            jquery_ui_css=jquery_ui_css,
            jquery_url=jquery_url,
            jquery_ui_url=jquery_ui_url,
            jqgrid_js=jqgrid_js)
        return page

    @cherrypy.expose
    def users_json(self, rows=None, sidx=None, _search=None, searchField=None,
        searchOper=None, searchString=None, page=None, sord=None, nd=None): # 1 line
        """Returns all users from test_data in a json format that's compatible with jqGrid.""" # 2 lines
        header = ["user", "password", "uid", "gid", "gecos", "homedir", "shell"] # 3 lines
        users_list = test_data_to_list(test_data) # 4 lines
        return jqgrid_json(self, users_list, header, rows=rows, sidx=sidx, _search=_search,
        searchField=searchField, searchOper=searchOper, searchString=searchString, page=page, sord=sord) # 5 lines!

conf = {
        '/': {
            'tools.staticdir.on': True,
            'tools.staticdir.root': os.getcwd(),
            'tools.staticdir.dir': ''
        },
    }
app = cherrypy.tree.mount(jqGrid(), config=conf)
cherrypy.quickstart(app)